package token

import (
	"context"
	"encoding/json"
	"errors"
	"fmt"
	"shop-common/library/variables"
	"shop-common/utils/contextArgs"
	"shop-security/cache"
	"shop-security/model"
	"time"
)

// device 是否一致
func IsInvalid(token *Claims, accessToken string) (*model.UserOnlineClient, bool) {
	user, err := cache.GetAuth(variables.RedisConn, fmt.Sprintf("%s:%s", AuthCacheSecret, accessToken))
	if err != nil || token == nil {
		return nil, false
	}

	return user, accessToken != "" && token.UserId != "" && user.AccessToken == accessToken && token.UserId == user.UserId
}

func SetAuthCache(accessToken, subject, userId string, id, shopId uint, device *contextArgs.Device) error {
	marshal, err := json.Marshal(&model.UserOnlineClient{
		UserId:      userId,
		Id:          id,
		ShopId:      shopId,
		State:       "online",
		Subject:     subject,
		Time:        time.Now().Format("2006-01-02 15:04:05"),
		Browser:     device.Browser,
		Os:          device.Os,
		Platform:    device.Platform,
		Ip:          device.Ip,
		AccessToken: accessToken,
	})

	if err != nil {
		return err
	}

	// 记录token
	err = cache.SetAuth(variables.RedisConn, fmt.Sprintf("%s:%s", AuthCacheSecret, accessToken), string(marshal), AuthCacheTimeoutUnix)
	return err
}

func GetCacheUser(accessToken string) (*model.UserOnlineClient, error) {
	cacheSigned, err := cache.GetAuth(variables.RedisConn, fmt.Sprintf("%s:%s", AuthCacheSecret, accessToken))
	// 验证token信息
	if err != nil {
		variables.Logger.Error(context.Background(), "cache get failed: err %v", err)
		return nil, errors.New("登陆已失效！")
	}

	return cacheSigned, nil
}

func ParseCacheUser(cacheSigned interface{}) (user *model.UserOnlineClient, err error) {
	value, ok := cacheSigned.(*model.UserOnlineClient)
	if !ok {
		return nil, errors.New("token为空")
	}


	return value, nil
}

func RefreshAccessToken(ctx context.Context, accessToken string, user *model.UserOnlineClient, device *contextArgs.Device) (string, error) {
	if device.Browser != user.Browser ||
		device.Os != user.Os ||
		device.Platform != user.Platform ||
		device.Ip != device.Ip {
		return "", errors.New("have been changed browser device")
	}

	// 生成新token
	_, auth, err := GenerateAccessToken(user.Subject, user.UserId)
	if err != nil {
		variables.Logger.Errorf(ctx, "refresh token failed: err %v", err)
		return "", err
	}

	go func() {
		_ = cache.DelAuth(variables.RedisConn, fmt.Sprintf("%s:%s", AuthCacheSecret, accessToken))
		_ = SetAuthCache(auth, "admin-srv", user.UserId, user.Id, user.ShopId, device)
	}()
	return auth, nil
}

func Destroy(ctx context.Context, accessToken string) error {
	return cache.DelAuth(variables.RedisConn, fmt.Sprintf("%s:%s", AuthCacheSecret, accessToken))
}

/*
这个机制只使用一个短期的Token,比如1天.
用户登陆后, 这个Token发给客户端, 用户每次请求就使用这个Token认证身份, Token过期后凭此token换取新的Token,一个过期的Token只能换取一个新的Token,这是关键. 如果Token被盗, 黑客要持续使用也需持续的换取新的Token, 服务器一旦发现,一个旧Token多次试图换取新Token,表示有异常. 这时强制用户再次登陆.

这个Token的有效期,针对不同的应用可以调整.
以设计招商银行的app为例:
1, 采用https 加密,确保传输安全.
2,Token的有效期设为15分钟,Token每15分钟,以旧换新换取新的Token. 正常情况下,这个以旧换新对用户不可见,一但两人试图旧换新换,两人都阻止,需要再次登陆.
3,对于修改密码和转账支付这样的关键操作,要求用户输入密码.
这样就万无一失了.

重复一下,设计安全机制时,一定要使用https, 没有https, 多数安全设计都是无用功
*/
